/**
 * Project: Mobile Asset Manager
 * Author: Sergey Budaev
 * Created: 18.12.2007 11:42:48
 * <p/>
 * Copyright (c) 1999-2008 Magenta Corporation Ltd. All Rights Reserved.
 * Magenta Technology proprietary and confidential.
 * Use is subject to license terms.
 */

package com.magenta.maxoptra.gis.utils;

import com.magenta.maxoptra.dto.Bound;
import com.magenta.maxoptra.dto.LatLonPoint;
import com.magenta.maxoptra.dto.PolygonCache;
import com.magenta.maxoptra.dto.SpatialSegment;
import com.magenta.maxoptra.utils.Point;
import org.joda.time.DateTime;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Hashtable;
import java.util.Map;
import java.util.Set;

import static java.lang.Math.*;

public class SpatialDataHelper {
    public static final int MINUTES_IN_DEGREE = 60;
    public static final double DEGREES_IN_RADIANS = 180 / PI;
    public static final double EARTH_RADIUS = 6371 * 1000;
    public static final Long ROUND_COEFFICIENT = 100000L;
    public static final Long LAT_COMPENSATOR_25 = 25L;
    public static final Long LAT_COMPENSATOR_110 = 110L;
    public static final int DEGREES_IN_OCTANT = 360 / 8;
    public static final String[] CARDINAL_POINTS_ALIASES = {"N", "NE", "E", "SE", "S", "SW", "W", "NW"};
    public static final double SPOTLIGHT_RADIUS = 30 * 1000;

    private static Hashtable<Long, Long> latCompensatorMap = new Hashtable<Long, Long>();

    private static boolean initTable = false;
    private static double[][] distTable = new double[181][2];

    public static boolean isPointInGeofence(
            double pointLatitude,
            double pointLongitude,
            double geofenceLatitude,
            double geofenceLongitude,
            double geofenceRadius
    ) {
        return getGeoDistance(pointLatitude, pointLongitude, geofenceLatitude, geofenceLongitude)
                <= geofenceRadius;
    }

    public static double getDistance(double latitude1, double longitude1, double latitude2, double longitude2) {
        longitude1 = minutesToRadians(longitude1);
        latitude1 = minutesToRadians(latitude1);

        longitude2 = minutesToRadians(longitude2);
        latitude2 = minutesToRadians(latitude2);

        return acos(
                sin(latitude1) * sin(latitude2)
                        + cos(latitude1) * cos(latitude2) * cos(longitude1 - longitude2)
        ) * EARTH_RADIUS;
    }

    public static double minutesToRadians(double minutes) {
        return degreesToRadians(minutesToDegrees(minutes));
    }

    public static double minutesToDegrees(double minutes) {
        return minutes / MINUTES_IN_DEGREE;
    }

    public static double degreesToMinutes(double degrees) {
        return degrees * MINUTES_IN_DEGREE;
    }

    public static double degreesToRadians(double degrees) {
        return degrees / DEGREES_IN_RADIANS;
    }

    public static String minutesToTraverse(int minutes) {
        int degrees = (int) minutesToDegrees(minutes);
        return degreesToTraverse(degrees);
    }

    public static String degreesToTraverse(int degrees) {
        if (degrees >= 360) {
            final int k = degrees / 360;
            degrees -= k * 360;
        }
        final int numberOfOctant = degrees / DEGREES_IN_OCTANT;
        final String cardinalPoint = CARDINAL_POINTS_ALIASES[numberOfOctant];
        degrees -= numberOfOctant * DEGREES_IN_OCTANT;
        return cardinalPoint + (degrees > 0 ? " " + degrees : "");
    }

    public static boolean isPointInRectangle(

            Double latitude,
            Double longitude,
            Double latitude1,
            Double longitude1,
            Double latitude2,
            Double longitude2
    ) {
        return !(longitude == null
                || latitude == null
                || longitude1 == null
                || longitude2 == null
                || latitude1 == null
                || latitude2 == null
        ) && longitude >= longitude1 && longitude <= longitude2
                && latitude >= latitude1 && latitude <= latitude2;
    }

    public static Long getDxLatitudeCoefficient(Double latitude) {
        return getDxLatitudeCoefficient(Math.round(latitude));
    }

    public static Long getDxLatitudeCoefficient(Long latitude) {
        return getDxLatitudeCoefficient(latitude, LAT_COMPENSATOR_25);

    }

    public static Long getDxLatitudeCoefficient(Long latitude, Long latitudeCompensator) {
        if (latCompensatorMap.isEmpty()) {
            initializeDxLatMap();
        }
        Long searchedCoeff = 0L;
        for (Map.Entry iterator : latCompensatorMap.entrySet()) {
            if (latitude < (Long) iterator.getKey()) {
                searchedCoeff = (Long) iterator.getValue();
                break;
            }
        }
        if (searchedCoeff == 0) {
            searchedCoeff = 1L;
        }
        return ((latitudeCompensator * latitude) / searchedCoeff);
    }

    public static Long getRoundValueToStored(double value) {
        return Math.round(value * ROUND_COEFFICIENT);

    }

    public static Double getStoredToDouble(long value) {
        return (double) value / ROUND_COEFFICIENT;
    }

    public static void initializeDxLatMap() {
        latCompensatorMap.put(0 * ROUND_COEFFICIENT, 111320L);
        latCompensatorMap.put(10 * ROUND_COEFFICIENT, 109640L);
        latCompensatorMap.put(20 * ROUND_COEFFICIENT, 104650L);
        latCompensatorMap.put(30 * ROUND_COEFFICIENT, 96490L);
        latCompensatorMap.put(40 * ROUND_COEFFICIENT, 85390L);
        latCompensatorMap.put(50 * ROUND_COEFFICIENT, 71700L);
        latCompensatorMap.put(60 * ROUND_COEFFICIENT, 55800L);
        latCompensatorMap.put(70 * ROUND_COEFFICIENT, 38190L);
        latCompensatorMap.put(80 * ROUND_COEFFICIENT, 19390L);
        latCompensatorMap.put(90 * ROUND_COEFFICIENT, 1L);
    }

    public static Long getLongitudeCoefficient() {
        if (latCompensatorMap.isEmpty()) {
            initializeDxLatMap();
        }
        return ((LAT_COMPENSATOR_25 * 180L * ROUND_COEFFICIENT) / latCompensatorMap.get(0L));
    }

    public static Double getLatitudeDeltaCoefficient(Long latitude) {
        initializeDxLatMap();
        Long searchedCoeff = 0L;
        for (Map.Entry iterator : latCompensatorMap.entrySet()) {
            if (latitude < (Long) iterator.getKey()) {
                searchedCoeff = (Long) iterator.getValue();
                break;
            }
        }
        if (searchedCoeff == 0) {
            searchedCoeff = 1L;
        }
        return (double) searchedCoeff / latCompensatorMap.get(0L);
    }

    public static boolean isPointInPolygon(Double latitude, Double longitude, PolygonCache polygonCache) {
        boolean result = false;

        if (polygonCache == null || longitude == null || latitude == null) {
            return false;
        }

        final Set<SpatialSegment> spatialSegments = polygonCache.getSpatialSegments(latitude);
        if (spatialSegments == null || spatialSegments.isEmpty()) {
            return false;
        }

        if (!isPointInRectangle(

                latitude,
                longitude,
                polygonCache.getMinLatitude(),
                polygonCache.getMinLongitude(),
                polygonCache.getMaxLatitude(),
                polygonCache.getMaxLongitude())

                ) {
            return false;
        }

        for (SpatialSegment spatialSegment : spatialSegments) {
            if (longitude.equals(spatialSegment.getLongitude1()) && latitude.equals(spatialSegment.getLatitude1())
                    || longitude.equals(spatialSegment.getLongitude2()) && latitude.equals(spatialSegment.getLatitude2())
                    ) {
                return true;
            }

            if ((Double.POSITIVE_INFINITY == spatialSegment.getCtg()
                    || Double.NEGATIVE_INFINITY == spatialSegment.getCtg())
                    && (latitude - spatialSegment.getLatitude1()) == 0) {
                return true;
            }

            final double longitudeX = (latitude - spatialSegment.getLatitude1()) * spatialSegment.getCtg()
                    + spatialSegment.getLongitude1();

            if (longitude <= longitudeX) {
                result = !result;
            }
        }
        return result;
    }

//    public static boolean isPointInSphere(Point center, Double radius, Point point) {
//        return Math.pow(point.getLatitude() - center.getLatitude(), 2) + Math.pow(point.getLongitude() - center.getLongitude(), 2) < Math.pow(radius, 2);
//    }

    //

    public static Bound getBoundAroundSphere(Double latitude, Double longitude, Double radiusKM) {
        float lati1;
        float longi1;
        float lati2;
        float longi2;
        if (!initTable) {
            initTableDistance();
            initTable = true;
        }
        int intLati = (int) Math.round(latitude);
        intLati = (int) checkBounds(intLati);
        double deltaLongitude = radiusKM / distTable[Math.abs(intLati)][1];
        longi1 = (float) (longitude - deltaLongitude);
        longi2 = (float) (longitude + deltaLongitude);

        int i = intLati;
        double dist = distTable[Math.abs(intLati)][0] * (intLati - latitude);
        while (dist < radiusKM) {
            i = (int) checkBounds(i);
            dist += distTable[Math.abs(i)][0];
            i++;
        }

        i = (int) checkBounds(i);
        lati1 = (float) (i - (dist - radiusKM) / distTable[Math.abs(i)][0]);

        i = intLati;
        dist = distTable[Math.abs(intLati)][0] * (latitude - intLati);
        while (dist < radiusKM) {
            i = (int) checkBounds(i);
            dist += distTable[Math.abs(i)][0];
            i--;
        }
        i = (int) checkBounds(i);
        lati2 = (float) (i + (dist - radiusKM) / distTable[Math.abs(i)][0]);

        return new Bound(lati1, longi1, lati2, longi2);
    }

    private static double checkBounds(double i) {
        if (i > 180) {
            i = 360 - i;
        } else if (i < -180) {
            i = -i - 360;
        }
        return i;
    }

    private static void initTableDistance() {
        double long1 = 0;
        double long2 = 1;
        for (int i = 0; i < 181; i++) {
            @SuppressWarnings({"UnnecessaryLocalVariable"})
            double lati1 = i;
            double lati2 = i + 1;

            distTable[i][0] = getGeoDistance(lati1, long1, lati2, long1);
            distTable[i][1] = getGeoDistance(lati1, long1, lati1, long2);
        }
    }

    public static Double[][] getPolygonPoints(Point[] points) {
        Double[][] pp = new Double[points.length][2];

        int i = 0;

        for (Point p : points) {
            pp[i][0] = p.getLatitude();
            pp[i][1] = p.getLongitude();
            i++;
        }

        return pp;
    }

    public static Double getGeoDistance(Long latitude1, Long longitude1, Long latitude2, Long longitude2) {
        return getGeoDistance(getStoredToDouble(latitude1), getStoredToDouble(longitude1), getStoredToDouble(latitude2), getStoredToDouble(longitude2));
    }

    public static double getGeoDistance(Double latitude1, Double longitude1, Double latitude2, Double longitude2) {
        if (longitude1.equals(longitude2) &&
                latitude1.equals(latitude2)) {
            return 0;
        }
        return getGeoDistance(
                latitude1.doubleValue(), longitude1.doubleValue(),
                latitude2.doubleValue(), longitude2.doubleValue()
        );
    }

    public static double getGeoDistance(double latitude1, double longitude1, double latitude2, double longitude2) {
        if (longitude1 == longitude2 &&
                latitude1 == latitude2) {
            return 0;
        }

        final double D2R = 0.017453; // Degrees 2 radians
        final double a = 6378137.0; // Main half-axis
        final double e2 = 0.006739496742337; // Ellipsoid excentricity square (ppc, let's make it right!)

        double fdLambda = (longitude1 - longitude2) * D2R;
        double fdPhi = (latitude1 - latitude2) * D2R;
        double fPhimean = ((latitude1 + latitude2) / 2.0) * D2R;
        double fTemp = 1 - e2 * (Math.pow(Math.sin(fPhimean), 2));
        double fRho = (a * (1 - e2)) / Math.pow(fTemp, 1.5);
        double fNu = a / (Math.sqrt(1 - e2 * (Math.sin(fPhimean) * Math.sin(fPhimean))));
        double fz = Math.sqrt(Math.pow(Math.sin(fdPhi / 2.0), 2) + Math.cos(latitude2 * D2R) *
                Math.cos(latitude1 * D2R) * Math.pow(Math.sin(fdLambda / 2.0), 2));
        fz = 2 * Math.asin(fz);
        double fAlpha = Math.cos(latitude2 * D2R) * Math.sin(fdLambda) * 1 / Math.sin(fz);
        fAlpha = Math.asin(fAlpha);
        double fR = (fRho * fNu) / ((fRho * Math.pow(Math.sin(fAlpha), 2)) + (fNu * Math.pow(Math.cos(fAlpha), 2)));
        return (fz * fR) / 1000; // in kilometers
    }

    //calculate distance between two points in radians
    public static double getDistanceInRadians(Double lat1, Double long1, Double lat2, Double long2) {
        lat1 = Math.toRadians(lat1);
        lat2 = Math.toRadians(lat2);
        long1 = Math.toRadians(long1);
        long2 = Math.toRadians(long2);
        return atan((sqrt(pow(cos(lat2) * sin(long2 - long1), 2) + pow(cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(long2 - long1), 2))) /
                (sin(lat1) * sin(lat2) + cos(lat1) * cos(lat2) * cos(long2 - long1)));
    }

    //calculate angle in degrees between three points (A,B,C). resulted angle is <ABC
    public static Double getAngleInDegrees(Point a, Point b, Point c) {
        double ab = SpatialDataHelper.getDistanceInRadians(a.getLatitude(), a.getLongitude(), b.getLatitude(), b.getLongitude());
        double bc = SpatialDataHelper.getDistanceInRadians(b.getLatitude(), b.getLongitude(), c.getLatitude(), c.getLongitude());
        double ac = SpatialDataHelper.getDistanceInRadians(a.getLatitude(), a.getLongitude(), c.getLatitude(), c.getLongitude());
        final double v1 = Math.sin(ab) * Math.sin(bc);
        if (v1 == 0) {
            return Double.NaN;
        }
        Double res = (Math.cos(ac) - Math.cos(ab) * Math.cos(bc)) / v1;
        if (Math.abs(res) > 1) {
            res = Math.signum(res);
        }
        res = new BigDecimal(res).setScale(5, RoundingMode.HALF_UP).doubleValue();
        Double v = Math.acos(res);
        return Math.toDegrees(v);
    }

    /**
     * @param a
     * @param distance - in metres
     * @return Bound
     */
    public static Bound getBoundInDistance(Point a, Double distance) {
        Double lonDelta = metresToLon(distance, a.getLatitude());
        Double latDelta = metresToLat(distance, a.getLatitude());

        Double northLon = a.getLongitude() + lonDelta;
        Double southLon = a.getLongitude() - lonDelta;
        Double westLat = a.getLatitude() + lonDelta;
        Double eastLat = a.getLatitude() - latDelta;

        return new Bound(westLat, northLon, eastLat, southLon);
    }

    public static double getDistanceByPoints(double longitude1, double latitude1, double longitude2, double latitude2) {

        //conversion to radian
        latitude1 = (latitude1 * 2.0 * Math.PI) / 360.0;
        longitude1 = (longitude1 * 2.0 * Math.PI) / 360.0;
        latitude2 = (latitude2 * 2.0 * Math.PI) / 360.0;
        longitude2 = (longitude2 * 2.0 * Math.PI) / 360.0;

        // use to different earth axis length
        double a = 6378137.0;        // Earth Major Axis (WGS84)
        double b = 6356752.3142;     // Minor Axis
        double f = (a - b) / a;        // "Flattening"
        double e = 2.0 * f - f * f;      // "Eccentricity"

        double beta = (a / Math.sqrt(1.0 - e * Math.sin(latitude1) * Math.sin(latitude1)));
        double cos = Math.cos(latitude1);
        double x = beta * cos * Math.cos(longitude1);
        double y = beta * cos * Math.sin(longitude1);
        double z = beta * (1 - e) * Math.sin(latitude1);

        beta = (a / Math.sqrt(1.0 - e * Math.sin(latitude2) * Math.sin(latitude2)));
        cos = Math.cos(latitude2);
        x -= (beta * cos * Math.cos(longitude2));
        y -= (beta * cos * Math.sin(longitude2));
        z -= (beta * (1 - e) * Math.sin(latitude2));

        return Math.sqrt((x * x) + (y * y) + (z * z));
    }

    public static Double getDeltaByDistance(double distance, double coord) {
        return distance / (111200 * Math.cos(coord * (Math.PI / 180)));
    }


    public static Bound getBound(Double latitude, Double longitude, Double distance) {
        distance = Math.sqrt(2 * Math.pow(distance, 2d));   // distance to SW and NE
        LatLonPoint sourcePoint = new LatLonPoint(latitude, longitude);
        LatLonPoint soutWestFromSource = sourcePoint.getPointInDistance(distance, 225d);
        LatLonPoint northEastFromSource = sourcePoint.getPointInDistance(distance, 45d);
        return new Bound(soutWestFromSource.getLat(), soutWestFromSource.getLon(),
                northEastFromSource.getLat(), northEastFromSource.getLon());
    }

    public static Double metresToLon(Double metres, Double latitude) {
        double p1 = 111412.84;
        double p2 = -93.5;
        double p3 = 0.118;
        Double lat = deg2rad(latitude);
        Double longlength = (p1 * Math.cos(lat)) + (p2 * Math.cos(3 * lat)) + (p3 * Math.cos(5 * lat)); // length of 1 degree in meters
        return metres / longlength;
    }

    public static Double metresToLat(Double metres, Double latitude) {
        double m1 = 111132.92;
        double m2 = -559.82;
        double m3 = 1.175;
        double m4 = -0.0023;
        Double lat = deg2rad(latitude);
        Double latlen = m1 + (m2 * Math.cos(2 * lat)) + (m3 * Math.cos(4 * lat)) + (m4 * Math.cos(6 * lat)); // length of 1 degree in meters
        return metres / latlen;
    }

    public static Double deg2rad(Double deg) {
        return (2.0 * Math.PI) / 360.0 * deg;
    }

    public static Double getCalculatedSpeed(Double latitude, Double longitude, Double prevLatitude, Double prevLongitude, DateTime time, DateTime prevTime) {
        if (prevLatitude!=null && prevLongitude!=null && prevTime!=null && time!=null) {
            Double distance = SpatialDataHelper.getGeoDistance(latitude, longitude, prevLatitude, prevLongitude);
            double timeDiff = (double)Math.abs(time.getMillis() - prevTime.getMillis()) / (double) DateTimeHelper.MSECS_IN_HOUR;
            if (timeDiff > 0) {
                return (double)(distance / timeDiff);
            }
        }
        return null;
    }

}
